/*
 * Copyright (c) 2003, KNOPFLERFISH project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials
 *   provided with the distribution.
 *
 * - Neither the name of the KNOPFLERFISH project nor the names of its
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.knopflerfish.service.log;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;

/**
 * LogRef is an utility class that simplifies the use of the LogService.
 * <P>
 * LogRef let you use the log without worrying about getting new service objects
 * when the log service is restarted. It also supplies methods with short names
 * that does logging with all the different LogService severity types.
 * </P>
 * <P>
 * To use the LogRef you need to import
 * <code>org.knopflerfish.service.log.LogRef</code> and instantiate LogRef
 * with your bundle context as parameter. The bundle context is used for getting
 * the LogService and adding a service listener.
 * </P>
 * <H2>Example usage</H2>
 * The <code>if</code> statement that protects each call to the
 * <code>LogRef</code> instance below is there to save the effort required for
 * creating the message string object in cases where the log will throw away the
 * log entry due to its unimportance. The user must have this <code>if</code>-test
 * in his code since that is the only way to avoid constructing the string
 * object. Placing it in the wrapper (LogRef) will not help due to the design of
 * the Java programming language.
 * 
 * <PRE>
 * package org.knopflerfish.example.hello;
 * 
 * import org.osgi.framework.*;
 * import org.knopflerfish.service.log.LogRef;
 * 
 * public class Hello implements BundleActivator {
 * 	LogRef log;
 * 
 * 	public void start(BundleContext bundleContext) {
 * 		log = new LogRef(bundleContext);
 * 		if (log.doInfo())
 * 			log.info(&quot;Hello started.&quot;);
 * 	}
 * 
 * 	public void stop(BundleContext bundleContext) {
 * 		if (log.doDebug())
 * 			log.debug(&quot;Hello stopped.&quot;);
 * 	}
 * }
 * </PRE>
 * 
 * @author Gatespace AB
 * @see org.osgi.service.log.LogService
 * @see org.knopflerfish.service.log.LogService
 */

public class LogRef implements ServiceListener, LogService {
	// Class name of the OSGI log service
	private final static String LOG_CLASS_OSGI = org.osgi.service.log.LogService.class
			.getName();

	// Class name of Knopflerfish extended log service
	private final static String LOG_CLASS_KF = org.knopflerfish.service.log.LogService.class
			.getName();

	private final static String logServiceFilter = "(|" + "(objectClass="
			+ LOG_CLASS_KF + ")(objectClass=" + LOG_CLASS_OSGI + "))";

	// Date formater used then sending entries to System.out
	private static SimpleDateFormat simpleDateFormat = null;

	// Handle to the framework
	private BundleContext bc;

	// Service reference for the current log service
	private ServiceReference logSR;

	// The current log service
	private org.osgi.service.log.LogService log;

	// If true and no log service, print on System.out
	private boolean useOut;

	// The id of the calling bundle
	private long bundleId;

	// If true warn about using closed LogRef object
	private boolean doWarnIfClosed;

	/**
	 * * Create new LogRef object for a given bundle. *
	 * 
	 * @param bc
	 *            the bundle context of the bundle that this log ref * instance
	 *            belongs too. *
	 * @param out
	 *            If true print messages on <code>System.out</code> when *
	 *            there is no log service.
	 */
	public LogRef(BundleContext bc, boolean out) {
		init(bc, out);
	}

	/**
	 * * Create new LogRef object for a given bundle. * *
	 * <p> * If the system property <tt>org.knopflerfish.log.out</tt> equals *
	 * "true", system.out will be used as fallback if no log service * is found. *
	 * </p> *
	 * 
	 * @param bc
	 *            the bundle context of the bundle that this log ref * instance
	 *            belongs too.
	 */
	public LogRef(BundleContext bc) {
		boolean b = false;

		try {
			b = "true".equals(System.getProperty("org.knopflerfish.log.out"));
		} catch (Throwable t) {
			System.err.println("get system property failed: " + t);
			t.printStackTrace();
		}

		init(bc, b);
	}

	private void init(BundleContext bc, boolean out) {
		this.bc = bc;
		useOut = out;
		bundleId = bc.getBundle().getBundleId();
		try {
			bc.addServiceListener(this, logServiceFilter);
		} catch (InvalidSyntaxException e) {
			error("Failed to register log service listener (filter="
					+ logServiceFilter + ")", e);
		}
	}

	/**
	 * * Service listener entry point. Releases the log service object if * one
	 * has been fetched. * *
	 * 
	 * @param evt
	 *            Service event
	 */
	public void serviceChanged(ServiceEvent evt) {
		if (evt.getServiceReference() == logSR
				&& evt.getType() == ServiceEvent.UNREGISTERING) {
			ungetLogService();
		}
	}

	/**
	 * * Unget the log service. Note that this method is synchronized on * the
	 * same object as the internal method that calls the actual log * service.
	 * This ensures that the log service is not removed by * this method while a
	 * log message is generated.
	 */
	private synchronized void ungetLogService() {
		doWarnIfClosed = doDebug();
		if (log != null) {
			bc.ungetService(logSR);
			logSR = null;
			log = null;
		}
	}

	/**
	 * * Close this LogRef object. Ungets the log service if active.
	 */
	public void close() {
		ungetLogService();
		bc.removeServiceListener(this);
		bc = null;
	}

	/**
	 * * Sends a message to the log if possible. * *
	 * 
	 * @param msg
	 *            Human readable string describing the condition. *
	 * @param level
	 *            The severity of the message (Should be one of the * four
	 *            predefined severities). *
	 * @param sr
	 *            The <code>ServiceReference</code> of the service * that this
	 *            message is associated with. *
	 * @param e
	 *            The exception that reflects the condition.
	 */
	protected synchronized void doLog(String msg, int level,
			ServiceReference sr, Throwable e) {
		if (bc != null && log == null) {
			logSR = bc.getServiceReference(LOG_CLASS_KF);
			if (logSR == null) {
				// No service implementing the Knopflerfish extended log, try to
				// look for a standard OSGi log service.
				logSR = bc.getServiceReference(LOG_CLASS_OSGI);
			}
			if (logSR != null) {
				log = (org.osgi.service.log.LogService) bc.getService(logSR);
			}
			if (log == null) {
				// Failed to get log service clear the service reference.
				logSR = null;
			}
		}
		if (log != null) {
			log.log(sr, level, msg, e);
		} else if (useOut || doWarnIfClosed) {
			if (bc == null) {
				System.err.println("WARNING! Bundle #" + bundleId
						+ " called closed LogRef object");
			}
			// No log service and request for messages on System.out
			System.out.print(LogUtil.fromLevel(level, 8));
			System.out.print(" ");
			if (simpleDateFormat == null) {
				simpleDateFormat = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
			}
			System.out.print(simpleDateFormat.format(new Date()));
			System.out.print(" ");
			System.out.print(getBundleName());
			System.out.print(" - ");
			if (sr != null) {
				System.out.print("[");
				System.out.print(sr);
				System.out.print("] ");
			}
			System.out.print(msg);
			if (e != null) {
				System.out.print(" (");
				System.out.print(e);
				System.out.print(")");

				System.out.println();
				e.printStackTrace();
			}
			System.out.println();
		}
	}

	/**
	 * * Returns the current log level. There is no use to generate log *
	 * entries with a severity level less than this value since such * entries
	 * will be thrown away by the log. * *
	 * 
	 * @return the current severity log level for this bundle.
	 */
	public int getLogLevel() {
		if (log != null && (log instanceof LogService)) {
			return ((LogService) log).getLogLevel();
		}
		return LOG_DEBUG;
	}

	/**
	 * * Returns true if messages with severity debug or higher * are saved by
	 * the log. *
	 * 
	 * @return <code>true</code> if messages with severity LOG_DEBUG * or
	 *         higher are included in the log, otherwise <code>false</code>.
	 */
	public boolean doDebug() {
		return getLogLevel() >= LOG_DEBUG;
	}

	/**
	 * * Returns true if messages with severity warning or higher * are saved by
	 * the log. *
	 * 
	 * @return <code>true</code> if messages with severity LOG_WARNING * or
	 *         higher are included in the log, otherwise <code>false</code>.
	 */
	public boolean doWarn() {
		return getLogLevel() >= LOG_WARNING;
	}

	/**
	 * * Returns true if messages with severity info or higher * are saved by
	 * the log. *
	 * 
	 * @return <code>true</code> if messages with severity LOG_INFO * or
	 *         higher are included in the log, otherwise <code>false</code>.
	 */
	public boolean doInfo() {
		return getLogLevel() >= LOG_INFO;
	}

	/**
	 * * Returns true if messages with severity error or higher * are saved by
	 * the log. *
	 * 
	 * @return <code>true</code> if messages with severity LOG_ERROR * or
	 *         higher are included in the log, otherwise <code>false</code>.
	 */
	public boolean doError() {
		return getLogLevel() >= LOG_ERROR;
	}

	/**
	 * * Log a debug level message * *
	 * 
	 * @param msg
	 *            Log message.
	 */
	public void debug(String msg) {
		doLog(msg, LOG_DEBUG, (ServiceReference) null, (Throwable) null);
	}

	/**
	 * * Log a debug level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param sr
	 *            The <code>ServiceReference</code> of the service * that this
	 *            message is associated with.
	 */
	public void debug(String msg, ServiceReference sr) {
		doLog(msg, LOG_DEBUG, sr, (Throwable) null);
	}

	/**
	 * * Log a debug level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param e
	 *            The exception that reflects the condition.
	 */
	public void debug(String msg, Throwable e) {
		doLog(msg, LOG_DEBUG, (ServiceReference) null, e);
	}

	/**
	 * * Log a debug level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param sr
	 *            The <code>ServiceReference</code> of the service * that this
	 *            message is associated with. *
	 * @param e
	 *            The exception that reflects the condition.
	 */
	public void debug(String msg, ServiceReference sr, Throwable e) {
		doLog(msg, LOG_DEBUG, sr, e);
	}

	/**
	 * * Log an info level message. * *
	 * 
	 * @param msg
	 *            Log message
	 */
	public void info(String msg) {
		doLog(msg, LOG_INFO, (ServiceReference) null, (Throwable) null);
	}

	/**
	 * * Log an info level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param sr
	 *            The <code>ServiceReference</code> of the service * that this
	 *            message is associated with.
	 */
	public void info(String msg, ServiceReference sr) {
		doLog(msg, LOG_INFO, sr, (Throwable) null);
	}

	/**
	 * * Log an info level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param e
	 *            The exception that reflects the condition.
	 */
	public void info(String msg, Throwable e) {
		doLog(msg, LOG_INFO, (ServiceReference) null, e);
	}

	/**
	 * * Log an info level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param sr
	 *            The <code>ServiceReference</code> of the service * that this
	 *            message is associated with. *
	 * @param e
	 *            The exception that reflects the condition.
	 */
	public void info(String msg, ServiceReference sr, Throwable e) {
		doLog(msg, LOG_INFO, sr, e);
	}

	/**
	 * * Log a warning level message. * *
	 * 
	 * @param msg
	 *            Log message
	 */
	public void warn(String msg) {
		doLog(msg, LOG_WARNING, (ServiceReference) null, (Throwable) null);
	}

	/**
	 * * Log a warning level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param sr
	 *            The <code>ServiceReference</code> of the service * that this
	 *            message is associated with.
	 */
	public void warn(String msg, ServiceReference sr) {
		doLog(msg, LOG_WARNING, sr, (Throwable) null);
	}

	/**
	 * * Log a warning level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param e
	 *            The exception that reflects the condition.
	 */
	public void warn(String msg, Throwable e) {
		doLog(msg, LOG_WARNING, (ServiceReference) null, e);
	}

	/**
	 * * Log a warning level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param sr
	 *            The <code>ServiceReference</code> of the service * that this
	 *            message is associated with. *
	 * @param e
	 *            The exception that reflects the condition.
	 */
	public void warn(String msg, ServiceReference sr, Throwable e) {
		doLog(msg, LOG_WARNING, sr, e);
	}

	/**
	 * * Log an error level message. * *
	 * 
	 * @param msg
	 *            Log message
	 */
	public void error(String msg) {
		doLog(msg, LOG_ERROR, (ServiceReference) null, (Throwable) null);
	}

	/**
	 * * Log an error level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param sr
	 *            The <code>ServiceReference</code> of the service * that this
	 *            message is associated with.
	 */
	public void error(String msg, ServiceReference sr) {
		doLog(msg, LOG_ERROR, sr, (Throwable) null);
	}

	/**
	 * * Log an error level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param e
	 *            The exception that reflects the condition.
	 */
	public void error(String msg, Throwable e) {
		doLog(msg, LOG_ERROR, (ServiceReference) null, e);
	}

	/**
	 * * Log an error level message. * *
	 * 
	 * @param msg
	 *            Log message *
	 * @param sr
	 *            The <code>ServiceReference</code> of the service * that this
	 *            message is associated with. *
	 * @param e
	 *            The exception that reflects the condition.
	 */
	public void error(String msg, ServiceReference sr, Throwable e) {
		doLog(msg, LOG_ERROR, sr, e);
	}

	/**
	 * * Log a message. * The ServiceDescription field and the Throwable * field
	 * of the LogEntry will be set to null. *
	 * 
	 * @param level
	 *            The severity of the message. (Should be one of the * four
	 *            predefined severities.) *
	 * @param message
	 *            Human readable string describing the condition.
	 */
	public void log(int level, String message) {
		doLog(message, level, (ServiceReference) null, (Throwable) null);
	}

	/**
	 * * Log a message with an exception. * The ServiceDescription field of the
	 * LogEntry will be set to null. *
	 * 
	 * @param level
	 *            The severity of the message. (Should be one of the * four
	 *            predefined severities.) *
	 * @param message
	 *            Human readable string describing the condition. *
	 * @param exception
	 *            The exception that reflects the condition.
	 */
	public void log(int level, String message, Throwable exception) {
		doLog(message, level, (ServiceReference) null, exception);
	}

	/**
	 * * Log a message associated with a specific Service. * The Throwable field
	 * of the LogEntry will be set to null. *
	 * 
	 * @param sr
	 *            The <code>ServiceReference</code> of the service that * this
	 *            message is associated with. *
	 * @param level
	 *            The severity of the message. (Should be one of the * four
	 *            predefined severities.) *
	 * @param message
	 *            Human readable string describing the condition.
	 */
	public void log(ServiceReference sr, int level, String message) {
		doLog(message, level, sr, (Throwable) null);
	}

	/**
	 * * Log a message with an exception associated with a specific Service. *
	 * 
	 * @param sr
	 *            The <code>ServiceReference</code> of the service that * this
	 *            message is associated with. *
	 * @param level
	 *            The severity of the message. (Should be one of the * four
	 *            predefined severities.) *
	 * @param message
	 *            Human readable string describing the condition. *
	 * @param exception
	 *            The exception that reflects the condition.
	 */
	public void log(ServiceReference sr, int level, String message,
			Throwable exception) {
		doLog(message, level, sr, exception);
	}

	/**
	 * * Returns a human readable name for the bundle that * <code>bc</code>
	 * represents. *
	 * 
	 * @return Name of the bundle that uses this wrapper * (at least 12
	 *         characters).
	 */
	private String getBundleName() {
		StringBuffer bundleName = new StringBuffer(24);
		// We can't get bundle-name since it requires AdminPermission.
		// bundleName.append((String)bc.getBundle().getHeaders().get("Bundle-Name"));
		// If name was not found use the Bid as name.
		if (bundleName.length() <= 0) {
			bundleName.append("bid#");
			bundleName.append(String.valueOf(bundleId));
		}
		if (bundleName.length() < 12) {
			bundleName.append("            ");
			bundleName.setLength(12);
		}
		return bundleName.toString();
	}

}
